package com.gitee.edodo.jenkins.plugin;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;

import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.exception.CosServiceException;
import com.qcloud.cos.model.COSObjectSummary;
import com.qcloud.cos.model.ListObjectsRequest;
import com.qcloud.cos.model.ObjectListing;
import com.qcloud.cos.region.Region;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import jenkins.tasks.SimpleBuildStep;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;


enum LogLevel {
    off, error, info, debug;

}


public class OSSPublisher extends Publisher implements SimpleBuildStep {

    private final String region;

    private final String secretId;

    private final String secretKey;

    private final String bucketName;

    private final String localPath;

    private final String pathOffset;

    private final String maxRetries;

    private LogLevel logLevel;//日志等级

    public String getRegion() {
        return region;
    }

    public String getSecretId() {
        return secretId;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public String getBucketName() {
        return bucketName;
    }

    public String getLocalPath() {
        return localPath;
    }

    public String getPathOffset() {
        return pathOffset;
    }

    public int getMaxRetries() {
        return StringUtils.isEmpty(maxRetries) ? 3 : Integer.parseInt(maxRetries);
    }

    public LogLevel getLogLevel() {
        return logLevel;
    }

    @DataBoundConstructor
    public OSSPublisher(String region, String secretId, String secretKey, String bucketName,
                        String localPath, String pathOffset, String maxRetries, String logLevel) {
        this.region = region;
        this.secretId = secretId;
        this.secretKey = secretKey;
        this.bucketName = bucketName;
        this.localPath = localPath;
        this.pathOffset = pathOffset;
        this.maxRetries = maxRetries;

        try {
            this.logLevel = LogLevel.valueOf(logLevel.toLowerCase());
        } catch (Exception ex) {
            this.logLevel = LogLevel.error;
        }
    }

    @Override
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }

    @Override
    public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) {
        PrintStream logger = listener.getLogger();
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
        // 2 设置 bucket 的地域
        // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分
        Region region = new Region(getRegion()); //COS_REGION 参数：配置成存储桶 bucket 的实际地域，例如 ap-beijing，更多 COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
        ClientConfig clientConfig = new ClientConfig(region);

        COSClient client = new COSClient(cred, clientConfig);

        Map<String, Long> key2LastModifiedMap = getLastModifies(client, logger);
        File localFile = new File(localPath);
        if (localFile.isDirectory()) {
            debugF(logger, "upload dir => %s\n", localFile.getAbsolutePath());
            upload(client, logger, localFile, key2LastModifiedMap);
            debugF(logger, "upload dir success\n");
        } else {
            debugF(logger, "upload file => %s\n", localFile.getAbsolutePath());
            uploadFile(client, logger, localFile, key2LastModifiedMap);
            debugF(logger, "upload file success\n");
        }
    }

    private void upload(COSClient client, PrintStream logger, File srcFile, Map<String, Long> key2LastModifiedMap) {
        if (srcFile.isDirectory()) {
            File[] files = srcFile.listFiles();
            if (files == null) {
                return;
            }
            for (File f : files) {
                upload(client, logger, f, key2LastModifiedMap);
            }
            return;
        }
        uploadFile(client, logger, srcFile, key2LastModifiedMap);
    }

    private void uploadFile(COSClient client, PrintStream logger, File srcFile, Map<String, Long> key2LastModifiedMap) {
        if (!srcFile.exists()) {
            debugF(logger, "file [%s] not exists, skipped\n", srcFile.getAbsolutePath());
            return;
        }
        int maxRetries = getMaxRetries();
        int retries = 0;
        do {
            if (retries > 0) {
                errorF(logger, "upload retrying (%d/%d)\n", retries, maxRetries);
            }
            try {
                uploadFile0(client, logger, srcFile, key2LastModifiedMap);
                return;
            } catch (Exception e) {
                e.printStackTrace(logger);
            }
        } while ((++retries) <= maxRetries);
        throw new RuntimeException("upload fail, more than the max of retries");
    }

    private void uploadFile0(COSClient client, PrintStream logger, File srcFile, Map<String, Long> key2LastModifiedMap) {
        if (!srcFile.getAbsolutePath().startsWith(pathOffset)) {
            errorF(logger, "upload error src file getAbsolutePath (%s) must start with %s\n", srcFile.getAbsolutePath(), pathOffset);
            return;
        }

        String realKey = srcFile.getAbsolutePath().substring(pathOffset.length()).replace("\\", "/");
        if (key2LastModifiedMap.containsKey(realKey)) {
            Long ossDate = key2LastModifiedMap.get(realKey);
            if (ossDate > srcFile.lastModified()) {
                debugF(logger, "[%s] to [%s] has uploaded file,skipped\n", srcFile.getAbsolutePath(), realKey);
                return;
            }
        }
//        if (realKey.startsWith("/")) {
//            realKey = realKey.substring(1);
//        }

        infoF(logger, "uploading [%s] to [%s]\n", srcFile.getAbsolutePath(), realKey);
        client.putObject(bucketName, realKey, srcFile);
    }

    @Symbol("QcloudCOSUpload")
    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {

        public FormValidation doCheckMaxRetries(@QueryParameter String value) {
            try {
                Integer.parseInt(value);
            } catch (Exception e) {
                return FormValidation.error(Messages.OSSPublish_MaxRetiesMustBeNumbers());
            }
            return FormValidation.ok();
        }

        public FormValidation doCheckRegion(@QueryParameter(required = true) String value) {
            return checkValue(value, Messages.OSSPublish_MissingRegion());
        }

        public FormValidation doCheckSecretId(@QueryParameter(required = true) String value) {
            return checkValue(value, Messages.OSSPublish_MissingSecretId());
        }

        public FormValidation doCheckSecretKey(@QueryParameter(required = true) String value) {
            return checkValue(value, Messages.OSSPublish_MissingSecretKey());
        }

        public FormValidation doCheckBucketName(@QueryParameter(required = true) String value) {
            return checkValue(value, Messages.OSSPublish_MissingBucketName());
        }

        public FormValidation doCheckLocalPath(@QueryParameter(required = true) String value) {
            return checkValue(value, Messages.OSSPublish_MissingLocalPath());
        }

        public FormValidation doCheckPathOffset(@QueryParameter(required = true) String value) {
            return checkValue(value, Messages.OSSPublish_MissingPathOffset());
        }

        private FormValidation checkBeginWithSlash(String value) {
            return FormValidation.ok();
        }

        private FormValidation checkValue(String value, String message) {
            if (StringUtils.isEmpty(value)) {
                return FormValidation.error(message);
            }
            return FormValidation.ok();
        }

        public FormValidation doCheckLogLevel(@QueryParameter(required = true) String loglevel) {
            try {
                LogLevel.valueOf(loglevel.toLowerCase());
            }catch (Exception e){
                return checkValue(loglevel, Messages.OSSPublish_ErrorLogLevel());
            }
            return FormValidation.ok();
        }

        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true;
        }

        @Nonnull
        @Override
        public String getDisplayName() {
            return Messages.OSSPublish_DisplayName();
        }
    }


    private Map<String, Long> getLastModifies(COSClient client, PrintStream logger) {
        ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
// 设置 bucket 名称
        listObjectsRequest.setBucketName(bucketName);
// prefix 表示列出的 object 的 key 以 prefix 开始
        listObjectsRequest.setPrefix(new File(localPath).getAbsolutePath().substring(pathOffset.length()).replace("\\", "/"));
// deliter 表示分隔符, 设置为/表示列出当前目录下的 object, 设置为空表示列出所有的 object
//        listObjectsRequest.setDelimiter("/");
// 设置最大遍历出多少个对象, 一次 listobject 最大支持1000
        listObjectsRequest.setMaxKeys(1000);
        Map<String, Long> key2LastModifiedMap = new HashMap<>();
        ObjectListing objectListing = null;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        do {
            try {
                objectListing = client.listObjects(listObjectsRequest);
            } catch (CosClientException e) {
                e.printStackTrace(logger);
                throw e;
            }

            // object summary 表示所有列出的 object 列表
            List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
            for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
                // 文件的路径 key
                String key = cosObjectSummary.getKey();

                debugF(logger, "old cos key：%s, lastModified:%s\n", key, LocalDateTime.ofInstant(cosObjectSummary.getLastModified().toInstant(), ZoneId.systemDefault()).format(formatter));
                key2LastModifiedMap.put(key, cosObjectSummary.getLastModified().getTime());
            }

            String nextMarker = objectListing.getNextMarker();
            listObjectsRequest.setMarker(nextMarker);
        } while (objectListing.isTruncated());
        return key2LastModifiedMap;
    }

    private void errorF(PrintStream logger, String format, Object... args) {
        if (logLevel != null && logLevel.ordinal() >= LogLevel.error.ordinal()) {
            logger.printf(format, args);
        }
    }

    private void infoF(PrintStream logger, String format, Object... args) {
        if (logLevel != null && logLevel.ordinal() >= LogLevel.info.ordinal()) {
            logger.printf(format, args);
        }
    }

    private void debugF(PrintStream logger, String format, Object... args) {
        if (logLevel != null && logLevel.ordinal() >= LogLevel.debug.ordinal()) {
            logger.printf(format, args);
        }
    }


}
